use core::f32;

use cgmath::{num_traits::{abs, float}, vec3, InnerSpace, Matrix4, One, Zero};
use getset::{CloneGetters, CopyGetters, Getters, MutGetters, Setters, WithSetters};


#[derive(Getters, Setters, WithSetters, MutGetters, CopyGetters, CloneGetters)]
pub struct NjmCamera {
    #[getset(get = "pub")]
    proj: cgmath::Matrix4<f32>,

    #[getset(get = "pub")]
    view: cgmath::Matrix4<f32>,

    #[getset(get = "pub")]
    view_inverse: cgmath::Matrix4<f32>,
}

impl NjmCamera {
    
    pub fn get_position(&self) -> cgmath::Vector3<f32> {
        return self.view_inverse[3].xyz();
    }

    pub fn set_orthographic_projection(&mut self, 
            left: f32, right: f32, top: f32, bottom: f32, near: f32, far: f32) {
        self.proj = cgmath::Matrix4::one();
        self.proj[0][0] = 2.0 / (right - left);
        self.proj[1][1] = 2.0 / (bottom - top);
        self.proj[2][2] = 1.0 / (far - near);
        self.proj[3][0] = -(right + left) / (right - left);
        self.proj[3][1] = -(bottom + top) / (bottom - top);
        self.proj[3][2] = -near / (far - near);
    }

    pub fn set_perspective_projection(&mut self, fovy: f32, aspect: f32, near: f32, far: f32) {
        let epsilon: f32 = f32::EPSILON * 10.0;
        assert!(abs(aspect - epsilon) > 0.0);
        let tan_half_fovy = (fovy / 2.0).tan();
        self.proj = cgmath::Matrix4::zero();
        self.proj[0][0] = 1.0 / (aspect * tan_half_fovy);
        self.proj[1][1] = 1.0 / (tan_half_fovy);
        self.proj[2][2] = far / (far - near);
        self.proj[2][3] = 1.0;
        self.proj[3][2] = -(far * near) / (far - near);
    }

    pub fn set_view_direction(&mut self, position: cgmath::Vector3<f32>, direction: cgmath::Vector3<f32>, up: cgmath::Vector3<f32>) {
        let view_dir: cgmath::Vector3<f32> =  direction.normalize();
        let view_right: cgmath::Vector3<f32> = view_dir.cross(up).normalize();
        let view_up: cgmath::Vector3<f32> = view_right.cross(view_dir).normalize();

        self.view = cgmath::Matrix4::one();
        self.view[0][0] = view_right.x;
        self.view[1][0] = view_right.y;
        self.view[2][0] = view_right.z;
        self.view[0][1] = view_up.x;
        self.view[1][1] = view_up.y;
        self.view[2][1] = view_up.z;
        self.view[0][2] = view_dir.x;
        self.view[1][2] = view_dir.y;
        self.view[2][2] = view_dir.z;
        self.view[3][0] = -view_right.dot(position);
        self.view[3][1] = -view_up.dot(position);
        self.view[3][2] = -view_dir.dot(position);

        self.view_inverse = cgmath::Matrix4::one();
        self.view_inverse[0][0] = view_right.x;
        self.view_inverse[0][1] = view_right.y;
        self.view_inverse[0][2] = view_right.z;
        self.view_inverse[1][0] = view_up.x;
        self.view_inverse[1][1] = view_up.y;
        self.view_inverse[1][2] = view_up.z;
        self.view_inverse[2][0] = view_dir.x;
        self.view_inverse[2][1] = view_dir.y;
        self.view_inverse[2][2] = view_dir.z;
        self.view_inverse[3][0] = position.x;
        self.view_inverse[3][1] = position.y;
        self.view_inverse[3][2] = position.z;
    }

    pub fn set_view_target(&mut self, position: cgmath::Vector3<f32>, target: cgmath::Vector3<f32>, up: cgmath::Vector3<f32>) {
        self.set_view_direction(position, target - position, up);
    }

    pub fn set_view_yxz(&mut self, position: cgmath::Vector3<f32>, rotation: cgmath::Vector3<f32>) {
        let c3 = rotation.z.cos();
        let s3 = rotation.z.sin();
        let c2 = rotation.x.cos();
        let s2 = rotation.x.sin();
        let c1 = rotation.y.cos();
        let s1 = rotation.y.sin();
        
        let view_right: cgmath::Vector3<f32> = vec3((c1 * c3 + s1 * s2 * s3), (c2 * s3), (c1 * s2 * s3 - c3 * s1));
        let view_up: cgmath::Vector3<f32> = vec3((c3 * s1 * s2 - c1 * s3), (c2 * c3), (c1 * c3 * s2 + s1 * s3));
        let view_dir: cgmath::Vector3<f32> =  vec3((c2 * s1), (-s2), (c1 * c2));

        self.view = cgmath::Matrix4::one();
        self.view[0][0] = view_right.x;
        self.view[1][0] = view_right.y;
        self.view[2][0] = view_right.z;
        self.view[0][1] = view_up.x;
        self.view[1][1] = view_up.y;
        self.view[2][1] = view_up.z;
        self.view[0][2] = view_dir.x;
        self.view[1][2] = view_dir.y;
        self.view[2][2] = view_dir.z;
        self.view[3][0] = -view_right.dot(position);
        self.view[3][1] = -view_up.dot(position);
        self.view[3][2] = -view_dir.dot(position);

        self.view_inverse = cgmath::Matrix4::one();
        self.view_inverse[0][0] = view_right.x;
        self.view_inverse[0][1] = view_right.y;
        self.view_inverse[0][2] = view_right.z;
        self.view_inverse[1][0] = view_up.x;
        self.view_inverse[1][1] = view_up.y;
        self.view_inverse[1][2] = view_up.z;
        self.view_inverse[2][0] = view_dir.x;
        self.view_inverse[2][1] = view_dir.y;
        self.view_inverse[2][2] = view_dir.z;
        self.view_inverse[3][0] = position.x;
        self.view_inverse[3][1] = position.y;
        self.view_inverse[3][2] = position.z;
    }

}

impl Default for NjmCamera {
    fn default() -> Self {
        return Self {
            proj: Matrix4::one(),
            view: Matrix4::one(),
            view_inverse: Matrix4::one(),
        };
    }
}